Skip to content

feat(trajectory): adjudicate redaction findings per item in bug reports - #381

Merged
0xKT merged 12 commits into
mainfrom
feat/redaction_review_flow
Sep 9, 2026
Merged

feat(trajectory): adjudicate redaction findings per item in bug reports#381
0xKT merged 12 commits into
mainfrom
feat/redaction_review_flow

Conversation

@forrestjgq

Copy link
Copy Markdown
Contributor

Summary

Bug reports previously classified redaction into clean / needs_review /
blocked, where a private-key hit refused to produce a report and the
needs_review confirmation showed at most 5 masked samples pointing at a
file buried inside the frozen snapshot. This PR folds blocked into a
per-item review flow:

  • Residual findings become review items, adjudicated by token value:
    keep as-is, or replace with [REDACTED:user-confirmed] (all four
    spelling variants replaced and verified gone, tar member names
    included - files carrying a redacted value are renamed with the same
    replacement their references got). Each item leads with the token and
    its occurrence count, groups identical context lines (every hit
    highlighted), and lists every location as semantic source plus
    file:line ("model call feat(security): label untrusted content and contain injection blast radius #2 input - artifacts/...json:57"); decisions
    are single-letter input (k/r/c) with no default. The user never has
    to open a file, and can still locate everything.
  • A private-key hit no longer blocks the report: the content is already
    replaced, the reporter acknowledges the risk per item, and the package
    carries an explicit security notice.
  • One risk-worded confirmation replaces the old create-then-review
    double prompt; --yes never implies the risk authorization. The
    summary shows a Reviewed tally (kept / redacted / acknowledged) with
    short masked tokens, keeping completed decisions apart from the
    warnings that still need consent.
  • Records and packages gain additive redaction fields security_notices
    and user_decisions (with a masked_token short identifier; absent on
    old records, validated when present); the persisted classification
    enum is unchanged, so old and new readers stay compatible both ways.
  • CLI: --keep-findings / --redact-findings (mutually exclusive) decide
    suspected items per kind for scripts; a run missing flags fails before
    any content change, listing every item and every missing flag at once.
  • Provider-generated call_ tool-call ids and the fixed usage field
    names (completion_tokens_details, prompt_tokens_details; exact
    literals only) join the residual-scan exemptions.
  • Untouched: the freeze pipeline (digests, byte-identical retries, crash
    recovery), the completeness probe, path sanitization, tar header
    anonymization, the expert command raven trajectory report, and the
    record state machine.

Note: this branch is stacked on feat/trajectory_bug_report (#380); the
diff includes those commits until #380 merges, after which this branch
will be rebased onto main.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • uv run pytest tests/test_trajectory_redact.py tests/test_trajectory_review.py tests/test_trajectory_bugreport.py tests/test_cli_trajectory_browse.py tests/test_cli_trajectory_commands.py -q -> 380 passed

  • uv run pytest tests/ --ignore=tests/integration -> 7156 passed (the 11 failures / 19 errors are pre-existing environment issues on this machine - missing tzdata and similar - identical on the base branch)

  • uv run --frozen --extra dev ruff check raven/ tests/ -> passed

  • uv run --frozen --extra dev ruff format --check raven/ tests/ -> 861 files already formatted

  • make check-large-files -> passed

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

User-visible changes: the blocked refusal wall is gone (replaced by an
informed acknowledgment with an in-package notice), the review
confirmation is a single risk-worded prompt instead of two, and
non-interactive review runs now require an explicit --keep-findings or
--redact-findings for suspected items (previously --accept-risk alone
shipped them as kept). Rollback: revert the squash commit; the new
metadata fields are additive, so already-produced packages and records
keep loading either way.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: stop the completeness probe from leaking SkillFileWatcher threads and crashing at process shutdown.

I reviewed the full github/main...HEAD diff and the surrounding bundle, replay, provider, pricing, store, CLI, and watcher call paths. I also checked the branch history, backward-compatible record validation, test changes for weakening, and the repository rules in AGENTS.md/CLAUDE.md plus the canonical trajectory terms in CONTEXT-MAP.md and CONTEXT.md. The per-item decision, redaction, path-sanitization, frozen-package, retry, and CLI authorization flows otherwise look internally consistent.

Verification: uv run pytest tests/test_trajectory_redact.py tests/test_trajectory_review.py tests/test_trajectory_bugreport.py tests/test_trajectory_completeness.py tests/test_cli_trajectory_commands.py tests/test_cli_trajectory_browse.py tests/test_provider_rates.py tests/test_token_wise_pricing.py -x reached 499 passed in 16.38s, then segfaulted and exited 139. The isolated completeness reproducer also prints 1 passed and exits 139; preventing ContextBuilder from starting its watcher makes that same two-replay reproducer exit 0.

evidence damage.
"""
try:
report = asyncio.run(run_replay(tree, mode="warn"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: each completeness probe leaks a live SkillFileWatcher, causing repeated probes and the changed test suite to segfault at process shutdown.

run_replay constructs an AgentLoop, whose default ContextBuilder starts the Rust-backed watcher, but neither replay nor this caller stops it. This is reproducible with uv run pytest tests/test_trajectory_completeness.py::test_complete_sample_probes_clean -q: it prints 1 passed but exits 139; after its two replay calls, threading.enumerate() shows two live SkillFileWatcher threads. Overriding ContextBuilder with start_watcher=False makes the same reproducer exit 0. The interactive trajectory browser can invoke this probe repeatedly, so the probe needs to avoid starting the watcher or tear the loop's watcher down.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bf934b0. The stop lives at the run_replay level rather than only in the probe: right after the AgentLoop is built, its ContextBuilder's skill watcher is stopped via the catalog's stop_file_watcher() (a replay feeds recorded content only, so the watcher has nothing to do), which covers the completeness probe, the replay CLI, and the regression runner in one place. The end-to-end recording tests in test_trajectory_replay.py stop the watcher on their five directly-built loops too (their leak pre-existed on the base branch), and a new regression test asserts run_replay leaves no SkillFileWatcher threads alive. Verification: your isolated reproducer (test_complete_sample_probes_clean) now exits 0, the full 8-file command reaches 499 passed with exit 0, and test_trajectory_replay.py alone is 32 passed exit 0 (previously 139).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: remove the transient PR-review provenance from the new test docstring before this revision merges.

The watcher leak itself is resolved by bf934b0. Stopping the watcher in run_replay covers the completeness probe and the other replay callers, the original isolated reproducer now exits 0, and the full changed-area run completes normally with 531 passing tests. The remaining blocker is separate from this thread and is marked on the new test docstring.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: remove the transient PR-review provenance from the new regression-test docstring.

The watcher lifecycle fix is correct at the shared run_replay layer: I checked the new delta, replay/completeness/browser callers, watcher stop semantics, branch history, backward compatibility, test changes for weakening, and the repository rules in AGENTS.md/CLAUDE.md plus the trajectory terminology in CONTEXT-MAP.md and CONTEXT.md. I found no other issue in this revision.

Verification: the original reproducer now completes with 1 passed and exit 0. uv run pytest tests/test_trajectory_redact.py tests/test_trajectory_review.py tests/test_trajectory_bugreport.py tests/test_trajectory_completeness.py tests/test_trajectory_replay.py tests/test_cli_trajectory_commands.py tests/test_cli_trajectory_browse.py tests/test_provider_rates.py tests/test_token_wise_pricing.py -x completed with 531 passed in 21.03s and exit 0.

Comment thread tests/test_trajectory_replay.py Outdated
async def test_run_replay_leaves_no_skill_watcher_threads(tmp_path) -> None:
"""The loop's ContextBuilder starts a skill file watcher; a replay must
stop it, or repeated probes leak daemon threads that crash the process at
interpreter shutdown (found by PR review on the completeness probe)."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: remove the transient PR-review provenance from this test docstring.

The parenthetical found by PR review is historical task provenance, not part of the behavior being specified. AGENTS.md section 1.1 explicitly forbids repository comments that reference a PR or describe transient task context because they become stale after the task. The rest of the docstring already explains the regression completely, so please drop only that parenthetical.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e363812: the parenthetical is gone, and the same cleanup was applied to three sibling docstrings that carried the same kind of transient wording (review-verified trap / reviewed-verified leak in test_trajectory_review.py, test_trajectory_bugreport.py, test_cli_trajectory_browse.py) - the behavior descriptions stand on their own now. The same commit also fixes the CI failure this test caused under the full-suite process: the assertion was global, counting watcher threads leaked by other suites in the same process; it now snapshots SkillFileWatcher threads before the replay and asserts the replay added none. Verified with a leaking suite run first in the same process (tests/test_decision_consumer.py + tests/test_trajectory_replay.py -> 41 passed, exit 0) and the replay file alone (32 passed, exit 0).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Confirmed: the transient provenance wording is gone, and scoping the watcher assertion to thread identities created during this replay preserves the regression check without depending on unrelated suite-owned watchers. The 531 changed-area tests pass with exit 0.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Both prior findings are resolved. I reviewed the new delta and rechecked its replay/completeness/browser callers, watcher lifecycle semantics, branch history, backward compatibility, test changes for weakening, and the repository rules in AGENTS.md/CLAUDE.md plus the trajectory terms in CONTEXT-MAP.md and CONTEXT.md. The delta-scoped watcher assertion remains capable of detecting a thread introduced by run_replay, while tolerating pre-existing watchers owned by other suites.

Verification: the four directly touched test files completed with 290 passed in 10.80s. The full changed-area command completed with 531 passed in 17.45s; both exited 0. A full uv run pytest -x run stopped after 1750 passed on tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare; that test also fails in isolation, and neither it nor its implementation is changed by this branch, so I did not attribute it to this revision.

forrestjgq1982 and others added 12 commits September 9, 2026 00:51
Residual findings now carry the plaintext token and every occurrence
(file, line number, line text, offsets) in memory, so the upcoming
review flow can adjudicate by value and render highlighted context.
redaction.json keeps its exact serialized shape - neither the token nor
the raw lines land on disk. Provider tool-call ids (call_ prefix) join
the residual-scan exemptions alongside tracing ids.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
New review module: build_review_items merges residual findings by token
value across every redaction report (one decision per value, since
replacement is global by value), names sources in human terms via the
bundle's span records, and derives the confirmed private-key item from
the replacement placeholders. validate_review_decisions is side-effect
free so interactive callers re-ask on linked-value conflicts before
anything is applied. apply_review_decisions counts kept tokens over the
body files before any change, replaces user-redacted values through the
existing exact-replacement engine, prunes them from the residual
findings before rewriting each tree's redaction.json, filters every
string headed for serialization, and verifies the result: redacted
variants gone everywhere, kept counts unchanged.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Fold the blocked classification into the review flow: a private-key hit
becomes a review reason plus a confirmed-sensitive item to acknowledge
(the content is already replaced), no longer a hard stop. freeze_export
gains a decide callback invoked when review items exist - decisions are
validated conflict-free before anything is applied, the problem fields
are read only after the decisions rewrote the redacted trees, the
completeness probe runs on the post-decision tree, and the export gate
also scans the package metadata for user-redacted spellings. Records
and packages carry the new additive redaction fields security_notices
and user_decisions (absent on old records, validated when present); a
report whose findings were all replaced stays needs_review so
risk_accepted remains truthful.

The browser and the CLI TTY path share one review screen: full item
list with semantic sources and highlighted context, keep/replace per
suspected item, acknowledge for confirmed ones, conflict re-asking
scoped to the linked group, and a single risk-worded confirmation that
--yes never skips. Without a TTY, --accept-risk acknowledges confirmed
items and keeps all findings (recorded as such); without it the command
fails and lists the reasons, leaving nothing behind.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Three fixes from the round-1 code review:

- A redacted value could still ship as a tar member name: artifact files
  are named after payload-derived basenames, and only contents were
  rewritten. apply_review_decisions now renames export paths with the
  exact replacement the content got (so the rewritten references keep
  resolving), aborts on rename collisions, filters kept findings' file
  fields and skipped-binary names before the summaries are rewritten,
  and verifies member paths alongside contents.
- Esc/Ctrl-C during CLI adjudication escaped as browser-internal
  exceptions; the CLI decide wrapper converts both to the review cancel,
  printing the cancel line and leaving nothing behind.
- Suspected items effectively defaulted to Keep (the pointer starts on
  the first select row): the row is now a placeholder that records
  nothing and re-asks, so a bare Enter cannot silently keep an unknown
  token; the private-key acknowledge default stays as designed.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
report-bug grows --keep-findings / --redact-findings (mutually
exclusive, checked before any collection side effect): each flag
pre-decides every suspected item, --accept-risk acknowledges the
confirmed-sensitive ones and keeps its role as the review-report
authorization. Without a TTY, missing flags fail at the decision point
against the original items - before any replacement or freeze work -
printing every item with its semantic sources and highlighted context
plus all missing flags at once, and leaving nothing behind. On a TTY
the flags skip the prompts for their kind while the risk consent still
gates a run without --accept-risk; --yes keeps meaning only the plain
creation confirmation.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Provider usage blocks carry fixed field names
(completion_tokens_details, prompt_tokens_details) that clear the
entropy bar in every trajectory with a model call; they join the
residual-scan exemptions as exact literals - any variation still flags.
User decisions gain a masked_token field (first/last four characters of
the adjudicated value; the placeholder for confirmed items) so screens
can show a short identifier instead of the whole sample window; the
field is optional on load, records written before it exist stay valid.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
…ions

Review items now lead with the plaintext token and its occurrence
count, group identical context lines (every hit on a short line stays
highlighted and counted), render whole lines up to 300 characters with
a 200-character window beyond that, and list every location as
"semantic source - file:line" under each context. Only context text is
capped at five groups: the remaining occurrences still list all their
locations, marked context omitted, and the screen header states what
the paths are relative to. Suspected decisions are now single-letter
input (k/r/c; empty or unknown input re-asks - no default), replacing
the placeholder select row; the confirmed item keeps Enter-equals-
acknowledge. The confirmation summary shows a Reviewed tally (kept /
redacted / acknowledged) with short masked tokens and aggregated
sources instead of raw sample windows, and a completed review adds a
plain-language line before the risk consent. Authorization semantics
are unchanged.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Two hits on a line just over the whole-line limit can render the same
windowed text from different positions; the context group now collects
every occurrence's spans (deduplicated) and the renderer merges
overlapping ranges before painting, so no hit loses its highlight to
display-text grouping.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
run_replay builds an AgentLoop whose ContextBuilder starts the
Rust-backed skill file watcher, and nothing ever stopped it: every
completeness probe leaked a daemon thread and repeated probes (or the
test suite) crashed the process at interpreter shutdown (exit 139).
A replay feeds recorded content only, so the watcher is stopped right
after the loop is built - covering the completeness probe, the replay
CLI, and the regression runner. The end-to-end recording tests stop
the watcher on their directly-built loops too, and a regression test
asserts a replay leaves no watcher threads behind.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The watcher regression test asserted zero SkillFileWatcher threads
globally, which fails under the full-suite CI process where other
suites hold their own watcher threads: it now snapshots the threads
before the replay and asserts the replay added none. Test docstrings
also drop the transient review-provenance wording; the behavior
descriptions stand on their own.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
@forrestjgq
forrestjgq force-pushed the feat/redaction_review_flow branch from e363812 to 3997f5a Compare September 9, 2026 00:52

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This is a history-only refresh: git diff e363812a253c HEAD is empty, so the reviewed source and test tree is unchanged. I refreshed the target ref and rechecked the actual github/main...HEAD diff and rewritten history against the repository rules; caller behavior, backward compatibility, architectural fit, and test strength remain as previously reviewed.

Verification: uv run pytest tests/test_trajectory_completeness.py::test_complete_sample_probes_clean tests/test_trajectory_replay.py::test_run_replay_leaves_no_skill_watcher_threads -q completed with 2 passed.

@0xKT
0xKT self-requested a review September 9, 2026 01:31
@0xKT
0xKT merged commit fd2a860 into main Sep 9, 2026
11 checks passed
@0xKT
0xKT deleted the feat/redaction_review_flow branch September 9, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants